The goal of this notebook is to compare pseudobulk and bulk calculations to determine which pseudobulk calculation should we proceed with for modeling: the sum of logcounts (pseudobulk_logcounts) or the DESeq2-normalized sum of raw counts (pseudobulk_deseq). To this end, we’ll visualize expression distributions, both on their own and compared to bulk TPM.

Setup

renv::load()

library(ggplot2)
theme_set(theme_bw())

Paths

data_dir <- here::here("analysis", "pseudobulk-bulk-prediction", "data")
tpm_dir <- file.path(data_dir, "tpm")
pseudobulk_dir <- file.path(data_dir, "pseudobulk")


tpm_files <- list.files(
  path = tpm_dir,
  full.names = TRUE,
  pattern = "*-tpm.tsv$"
)
tpm_names <- stringr::str_split_i(basename(tpm_files), pattern = "-", i = 1)
names(tpm_files) <- tpm_names


pseudobulk_files <- list.files(
  path = pseudobulk_dir,
  full.names = TRUE,
  pattern = "*-pseudobulk.rds$"
)
pseudobulk_names <- stringr::str_split_i(basename(pseudobulk_files), pattern = "-", i = 1)
names(pseudobulk_files) <- pseudobulk_names

# Make sure we have the same projects, in the same order
stopifnot(
  all.equal(names(tpm_files), names(pseudobulk_files))
)

Read and prepare input data

Data are saved as matrices, so we’ll convert them to per-project long data frames here.

tpm_df_list <- tpm_files |>
  purrr::map(
    \(tpm_file) {
      readr::read_tsv(tpm_file, show_col_types = FALSE) |>
        tidyr::pivot_longer(
          -ensembl_id, 
          names_to = "sample_id", 
          values_to = "expression"
        ) |>
        dplyr::mutate(expression_type = "bulk_tpm")   
   }
  )

project_df_list <- purrr::map2(
  pseudobulk_files, 
  tpm_df_list,
  \(pseudo_file, tpm_df) {
    
    pseudo_list <- readr::read_rds(pseudo_file)
    
    # Filter tpm_df for genes in the pseudobulk
    tpm_filtered_df <- tpm_df |>
      dplyr::filter(ensembl_id %in% rownames(pseudo_list[[1]]))
      
    # combine pseudobulk matrices into a long data frame and bind with tpm
    combined_df_long <- pseudo_list |>
      purrr::map(
        \(mat) {
          mat |>
            as.data.frame() |> 
            tibble::rownames_to_column("ensembl_id") |> 
            tidyr::pivot_longer(
              -ensembl_id, 
              names_to = "sample_id", 
              values_to = "expression"
            )
        }
      ) |>
      purrr::list_rbind(names_to = "expression_type") |> 
      dplyr::bind_rows(tpm_filtered_df) |>
      dplyr::select(sample_id, ensembl_id, expression, expression_type)
  }
)

shared_genes <- purrr::map(
  project_df_list, 
  \(df) df$ensembl_id
)

tpm_df_indicator_list <- purrr::map2(
  tpm_df_list,
  shared_genes,
  \(df, ids) dplyr::mutate(df, in_pseudo = ensembl_id %in% ids)
  )

# clean up!
rm(shared_genes)
rm(tpm_df_list)

Bulk TPM distributions

When establishing pseudobulk datasets, genes with low expression levels were removed.

Before looking at pseudobulk measures here, we’ll look at how the TPM distributions (log2) compared between for genes that are and are not included in the pseudobulk datasets. the comparisons. We expect to see that genes not included in the pseudobulk have lower TPM.

tpm_df_indicator_list |>
  purrr::imap(
    \(df, project_id) {
      
      ggplot(df) + 
        aes(x = log2(expression), fill = in_pseudo) + 
        geom_density(alpha = 0.5) + 
        labs(
          title = project_id, 
          x = "log2 bulk TPM"
        ) +
        theme(legend.position = "bottom")
    }
  ) |>
  patchwork::wrap_plots(guides = "collect", nrow = 1) & theme(legend.position = "bottom")

We mostly see the expected trend: Genes included in the pseudobulk counts have higher expression in bulk compared to genes which were removed. SCPCP000009 is an exception here, but this project has fewer samples compared to the rest so this could be a power artifact:

tpm_df_indicator_list |>
  purrr::map(
    \(df) length(unique(df$sample_id))
  )
$SCPCP000001
[1] 18

$SCPCP000002
[1] 20

$SCPCP000006
[1] 39

$SCPCP000009
[1] 3

$SCPCP000017
[1] 24

Pause to clean up a little!

rm(tpm_df_indicator_list)

Distributions of pseudobulk

First, let’s get a general sense of the scale of values in the pseudobulk quantities:

## logcounts
project_df_list |>
  purrr::map(
    \(df) {
      df |> 
        dplyr::filter(expression_type == "pseudobulk_logcounts") |>
        dplyr::pull(expression) |> 
        summary()
  })
$SCPCP000001
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
    0.000     3.588    29.944   219.697   194.455 23756.843 

$SCPCP000002
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
    0.00     4.82    37.60   286.34   225.83 47645.99 

$SCPCP000006
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
     0.00      3.47     23.37    250.10    147.66 138125.78 

$SCPCP000009
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
     0.00     12.47     53.09    376.99    219.65 194487.03 

$SCPCP000017
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
    0.00     3.31    32.08   343.49   290.80 99994.82 
## deseq
project_df_list |>
  purrr::map(
    \(df) {
      df |> 
        dplyr::filter(expression_type == "pseudobulk_deseq") |>
        dplyr::pull(expression) |> 
        summary()
  })
$SCPCP000001
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -2.508   2.354   5.394   5.188   7.881  20.706 

$SCPCP000002
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -1.722   2.784   5.577   5.458   7.942  19.239 

$SCPCP000006
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -2.510   1.986   4.661   4.659   7.182  18.286 

$SCPCP000009
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 0.1023  3.9929  6.1419  6.1122  8.0571 19.7752 

$SCPCP000017
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
 -2.330   2.093   5.243   5.226   8.288  22.893 

The logcounts version has some strong right-skew, so we should log those values to support modeling. We don’t see the same level of skew from DESeq, which is good since we don’t expect to see it out of that normalization procedure; we don’t need to transform those.

Let’s therefore update expression to be log2 for both TPM and pseudobulk logcounts (i.e., for all but pseudobulk deseq) since we’ll use those scales moving forward:

project_df_list <- project_df_list |>
  purrr::map(
    \(df) {
        df |>
          dplyr::mutate(expression = ifelse(
            stringr::str_detect(expression_type, "deseq"),
            expression,
            log2(expression) # TODO: do we want a fudge here?
          ))
    }
  )

Now, we can visualize distributions of all quantities:

project_df_list |>
  purrr::imap(
    \(df, project_id) {
      
     ggplot(df) + 
        aes(x = expression, fill = expression_type) + 
        geom_density(alpha = 0.5) + 
        scale_fill_brewer(palette = "Dark2") + 
        facet_wrap(vars(expression_type), scales = "free", nrow = 3) +
        ggtitle(project_id) +
        theme(legend.position = "none")
    }
  ) |>
  patchwork::wrap_plots(guides = "collect", nrow = 1)
Warning: Removed 140153 rows containing non-finite outside the scale range
(`stat_density()`).
Warning: Removed 131868 rows containing non-finite outside the scale range
(`stat_density()`).
Warning: Removed 416112 rows containing non-finite outside the scale range
(`stat_density()`).
Warning: Removed 17186 rows containing non-finite outside the scale range
(`stat_density()`).
Warning: Removed 487720 rows containing non-finite outside the scale range
(`stat_density()`).

Both the pseudobulk distributions look reasonable enough!

Relationship between quantities

Now come the many plots: What does the relationship look like between bulk and each flavor of pseudobulk? We’ll visualize this per sample.

We’ll first make a pivoted version of this data to enable plotting and modeling.

project_df_wide_list <- project_df_list |>
  purrr::imap(
    \(df, project_id) {
       df |> 
        tidyr::pivot_wider(
          names_from = expression_type, 
          values_from = expression
        )
    })

Next, we have many plots, organized by project:

project_df_wide_list |>
  purrr::imap(
    \(df, project_id) {
      
      p1 <- ggplot(df) + 
        aes(x = pseudobulk_deseq, y = bulk_tpm) + 
        geom_point(alpha = 0.2, size = 0.5) + 
        geom_smooth(method = "lm") + 
        facet_wrap(vars(sample_id), nrow = 5) +
        ggtitle("bulk tpm ~ deseq")
      
      p2 <- ggplot(df) + 
        aes(x = pseudobulk_logcounts, y = bulk_tpm) + 
        geom_point(alpha = 0.2, size = 0.5) + 
        geom_smooth(method = "lm") +
        facet_wrap(vars(sample_id), nrow = 5) +
        ggtitle("bulk tpm ~ logcounts") 
      
       p3 <- ggplot(df) + 
        aes(x = pseudobulk_logcounts, y = pseudobulk_deseq) + 
        geom_point(alpha = 0.2, size = 0.5) + 
        geom_smooth(method = "lm") +
        facet_wrap(vars(sample_id), nrow = 5) +
        ggtitle("deseq ~ logcounts") 
           
      
      patchwork::wrap_plots(p1, p2, p3) 

    }
  ) |>
  patchwork::wrap_plots(nrow = 5)

On the whole, the relationship between bulk and pseudobulk look exceptionally similar regardless of which pseudobulk quantity is used! On the right-side, we see that indeed the pseudobulk versions are highly similar to one another (right-side panels). The main difference we can pick out from these plots is that the logcounts version has a band of lowly-expressed genes which the deseq version doesn’t appear to show, which again is consistent with their different origins.

To identify if there’s a meaningful benefit to using one pseudobulk version over another, we’ll build linear models of tpm ~ pseudobulk * sample; we can assume an interaction based on the slopes in the above scatterplots. Note that these are not final models for analysis, but only used to guide our next steps of actually building/fine-tuning the models.

We’ll also have a look at the distributions of model residuals along the way.

project_df_wide_list |>
  purrr::map(
    \(df) {
      
      # Build models
      # We first need to remove any undefined values for each model
      df_deseq <- df |>
        dplyr::filter(is.finite(pseudobulk_deseq), is.finite(bulk_tpm))
      fit_deseq <- lm(bulk_tpm ~ pseudobulk_deseq * sample_id, data = df_deseq)
    
      df_logcounts <- df |>
        dplyr::filter(is.finite(pseudobulk_logcounts), is.finite(bulk_tpm))
      fit_logcounts <- lm(bulk_tpm ~ pseudobulk_logcounts * sample_id, data = df_logcounts)
      
      # Tabulate some fit stats
      fit_table <- tibble::tribble(
        ~expression_type, ~rsquared, ~residual_sd, 
        "deseq", broom::glance(fit_deseq)$r.squared, broom::glance(fit_deseq)$sigma, 
        "logcounts", broom::glance(fit_logcounts)$r.squared, broom::glance(fit_logcounts)$sigma
      )
      
      
      # Tabulate some fit stats, wide for easier viewing
      broom_deseq <- broom::glance(fit_deseq)
      broom_logcounts <- broom::glance(fit_logcounts)
      fit_table <- data.frame(
        deseq_rsquared        = broom_deseq$r.squared,
        logcounts_rsquared    = broom_logcounts$r.squared,
        deseq_residual_sd     = broom_deseq$sigma,
        logcounts_residual_sd = broom_logcounts$sigma    
      )
      
      # Plot the residuals as well on the way down
      deseq_augment <- broom::augment(fit_deseq)
      logcounts_augment <- broom::augment(fit_logcounts)
      
      p1 <- ggplot(deseq_augment) + 
        aes(x = pseudobulk_deseq, y = .resid) + 
        geom_point(size = 0.5, alpha = 0.5) + 
        geom_hline(yintercept = 0, color = "red") +
        ggtitle("deseq")
      p2 <- ggplot(logcounts_augment) + 
        aes(x = pseudobulk_logcounts, y = .resid) + 
        geom_point(size = 0.5, alpha = 0.5) + 
        geom_hline(yintercept = 0, color = "red") +
        ggtitle("logcounts")
      
      print(patchwork::wrap_plots(p1, p2, nrow = 1))
      
      # actually return the fit info
      fit_table
      
    }
) |>
  purrr::list_rbind(names_to = "project_id")

Models from different pseudobulk quantities are expectedly, based on scatterplots above, minimal, and based on the residual plots around linear model assumptions seem reasonably met. The DESeq2 flavor appears to marginally outperform (based on these limited stats) in 3/5 projects.

Conclusions

Either calculation for pseudobulk appears to be fine, and it’s likely that we’d get roughly the same results either way. I would suggest to proceed with the DESeq2 normalized version since we do not need additional log2 transformations, which leads to loss of 0-expression genes in the model since they are undefined (unless we want to fudge factor them, but avoiding this if we can seems best).

Session info

sessionInfo()
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS 15.2

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] ggplot2_3.5.1

loaded via a namespace (and not attached):
 [1] sass_0.4.9          generics_0.1.3      tidyr_1.3.1        
 [4] renv_1.0.11         stringi_1.8.4       lattice_0.22-6     
 [7] hms_1.1.3           digest_0.6.37       magrittr_2.0.3     
[10] evaluate_1.0.1      grid_4.4.0          RColorBrewer_1.1-3 
[13] fastmap_1.2.0       Matrix_1.7-1        rprojroot_2.0.4    
[16] jsonlite_1.8.9      backports_1.5.0     BiocManager_1.30.25
[19] mgcv_1.9-1          purrr_1.0.2         scales_1.3.0       
[22] jquerylib_0.1.4     cli_3.6.3           rlang_1.1.4        
[25] crayon_1.5.3        bit64_4.5.2         munsell_0.5.1      
[28] splines_4.4.0       withr_3.0.2         cachem_1.1.0       
[31] yaml_2.3.10         tools_4.4.0         parallel_4.4.0     
[34] tzdb_0.4.0          dplyr_1.1.4         colorspace_2.1-1   
[37] here_1.0.1          broom_1.0.7         vctrs_0.6.5        
[40] R6_2.5.1            lifecycle_1.0.4     stringr_1.5.1      
[43] bit_4.5.0.1         vroom_1.6.5         pkgconfig_2.0.3    
[46] pillar_1.10.0       bslib_0.8.0         gtable_0.3.6       
[49] glue_1.8.0          xfun_0.49           tibble_3.2.1       
[52] tidyselect_1.2.1    knitr_1.49          farver_2.1.2       
[55] htmltools_0.5.8.1   nlme_3.1-166        patchwork_1.3.0    
[58] rmarkdown_2.29      labeling_0.4.3      readr_2.1.5        
[61] compiler_4.4.0     
LS0tCnRpdGxlOiAiUHNldWRvYnVsayBhbmQgYnVsayBkYXRhIGRpc3RyaWJ1dGlvbnMiCmF1dGhvcjogU3RlcGhhbmllIEouIFNwaWVsbWFuCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0aDogMwogICAgY29kZV9mb2xkaW5nOiBzaG93Ci0tLQoKVGhlIGdvYWwgb2YgdGhpcyBub3RlYm9vayBpcyB0byBjb21wYXJlIHBzZXVkb2J1bGsgYW5kIGJ1bGsgY2FsY3VsYXRpb25zIHRvIGRldGVybWluZSB3aGljaCBwc2V1ZG9idWxrIGNhbGN1bGF0aW9uIHNob3VsZCB3ZSBwcm9jZWVkIHdpdGggZm9yIG1vZGVsaW5nOiB0aGUgc3VtIG9mIGxvZ2NvdW50cyAoYHBzZXVkb2J1bGtfbG9nY291bnRzYCkgb3IgdGhlIGBERVNlcTJgLW5vcm1hbGl6ZWQgc3VtIG9mIHJhdyBjb3VudHMgKGBwc2V1ZG9idWxrX2Rlc2VxYCkuClRvIHRoaXMgZW5kLCB3ZSdsbCB2aXN1YWxpemUgZXhwcmVzc2lvbiBkaXN0cmlidXRpb25zLCBib3RoIG9uIHRoZWlyIG93biBhbmQgY29tcGFyZWQgdG8gYnVsayBUUE0uIAoKCgojIyBTZXR1cAoKYGBge3Igc2V0dXB9CnJlbnY6OmxvYWQoKQoKbGlicmFyeShnZ3Bsb3QyKQp0aGVtZV9zZXQodGhlbWVfYncoKSkKYGBgCgojIyMgUGF0aHMKCmBgYHtyIHBhdGhzfQpkYXRhX2RpciA8LSBoZXJlOjpoZXJlKCJhbmFseXNpcyIsICJwc2V1ZG9idWxrLWJ1bGstcHJlZGljdGlvbiIsICJkYXRhIikKdHBtX2RpciA8LSBmaWxlLnBhdGgoZGF0YV9kaXIsICJ0cG0iKQpwc2V1ZG9idWxrX2RpciA8LSBmaWxlLnBhdGgoZGF0YV9kaXIsICJwc2V1ZG9idWxrIikKCgp0cG1fZmlsZXMgPC0gbGlzdC5maWxlcygKICBwYXRoID0gdHBtX2RpciwKICBmdWxsLm5hbWVzID0gVFJVRSwKICBwYXR0ZXJuID0gIiotdHBtLnRzdiQiCikKdHBtX25hbWVzIDwtIHN0cmluZ3I6OnN0cl9zcGxpdF9pKGJhc2VuYW1lKHRwbV9maWxlcyksIHBhdHRlcm4gPSAiLSIsIGkgPSAxKQpuYW1lcyh0cG1fZmlsZXMpIDwtIHRwbV9uYW1lcwoKCnBzZXVkb2J1bGtfZmlsZXMgPC0gbGlzdC5maWxlcygKICBwYXRoID0gcHNldWRvYnVsa19kaXIsCiAgZnVsbC5uYW1lcyA9IFRSVUUsCiAgcGF0dGVybiA9ICIqLXBzZXVkb2J1bGsucmRzJCIKKQpwc2V1ZG9idWxrX25hbWVzIDwtIHN0cmluZ3I6OnN0cl9zcGxpdF9pKGJhc2VuYW1lKHBzZXVkb2J1bGtfZmlsZXMpLCBwYXR0ZXJuID0gIi0iLCBpID0gMSkKbmFtZXMocHNldWRvYnVsa19maWxlcykgPC0gcHNldWRvYnVsa19uYW1lcwoKIyBNYWtlIHN1cmUgd2UgaGF2ZSB0aGUgc2FtZSBwcm9qZWN0cywgaW4gdGhlIHNhbWUgb3JkZXIKc3RvcGlmbm90KAogIGFsbC5lcXVhbChuYW1lcyh0cG1fZmlsZXMpLCBuYW1lcyhwc2V1ZG9idWxrX2ZpbGVzKSkKKQpgYGAKCiMjIyBSZWFkIGFuZCBwcmVwYXJlIGlucHV0IGRhdGEKCgpEYXRhIGFyZSBzYXZlZCBhcyBtYXRyaWNlcywgc28gd2UnbGwgY29udmVydCB0aGVtIHRvIHBlci1wcm9qZWN0IGxvbmcgZGF0YSBmcmFtZXMgaGVyZS4gCgpgYGB7cn0KdHBtX2RmX2xpc3QgPC0gdHBtX2ZpbGVzIHw+CiAgcHVycnI6Om1hcCgKICAgIFwodHBtX2ZpbGUpIHsKICAgICAgcmVhZHI6OnJlYWRfdHN2KHRwbV9maWxlLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKSB8PgogICAgICAgIHRpZHlyOjpwaXZvdF9sb25nZXIoCiAgICAgICAgICAtZW5zZW1ibF9pZCwgCiAgICAgICAgICBuYW1lc190byA9ICJzYW1wbGVfaWQiLCAKICAgICAgICAgIHZhbHVlc190byA9ICJleHByZXNzaW9uIgogICAgICAgICkgfD4KICAgICAgICBkcGx5cjo6bXV0YXRlKGV4cHJlc3Npb25fdHlwZSA9ICJidWxrX3RwbSIpICAgCiAgIH0KICApCgpwcm9qZWN0X2RmX2xpc3QgPC0gcHVycnI6Om1hcDIoCiAgcHNldWRvYnVsa19maWxlcywgCiAgdHBtX2RmX2xpc3QsCiAgXChwc2V1ZG9fZmlsZSwgdHBtX2RmKSB7CiAgICAKICAgIHBzZXVkb19saXN0IDwtIHJlYWRyOjpyZWFkX3Jkcyhwc2V1ZG9fZmlsZSkKICAgIAogICAgIyBGaWx0ZXIgdHBtX2RmIGZvciBnZW5lcyBpbiB0aGUgcHNldWRvYnVsawogICAgdHBtX2ZpbHRlcmVkX2RmIDwtIHRwbV9kZiB8PgogICAgICBkcGx5cjo6ZmlsdGVyKGVuc2VtYmxfaWQgJWluJSByb3duYW1lcyhwc2V1ZG9fbGlzdFtbMV1dKSkKICAgICAgCiAgICAjIGNvbWJpbmUgcHNldWRvYnVsayBtYXRyaWNlcyBpbnRvIGEgbG9uZyBkYXRhIGZyYW1lIGFuZCBiaW5kIHdpdGggdHBtCiAgICBjb21iaW5lZF9kZl9sb25nIDwtIHBzZXVkb19saXN0IHw+CiAgICAgIHB1cnJyOjptYXAoCiAgICAgICAgXChtYXQpIHsKICAgICAgICAgIG1hdCB8PgogICAgICAgICAgICBhcy5kYXRhLmZyYW1lKCkgfD4gCiAgICAgICAgICAgIHRpYmJsZTo6cm93bmFtZXNfdG9fY29sdW1uKCJlbnNlbWJsX2lkIikgfD4gCiAgICAgICAgICAgIHRpZHlyOjpwaXZvdF9sb25nZXIoCiAgICAgICAgICAgICAgLWVuc2VtYmxfaWQsIAogICAgICAgICAgICAgIG5hbWVzX3RvID0gInNhbXBsZV9pZCIsIAogICAgICAgICAgICAgIHZhbHVlc190byA9ICJleHByZXNzaW9uIgogICAgICAgICAgICApCiAgICAgICAgfQogICAgICApIHw+CiAgICAgIHB1cnJyOjpsaXN0X3JiaW5kKG5hbWVzX3RvID0gImV4cHJlc3Npb25fdHlwZSIpIHw+IAogICAgICBkcGx5cjo6YmluZF9yb3dzKHRwbV9maWx0ZXJlZF9kZikgfD4KICAgICAgZHBseXI6OnNlbGVjdChzYW1wbGVfaWQsIGVuc2VtYmxfaWQsIGV4cHJlc3Npb24sIGV4cHJlc3Npb25fdHlwZSkKICB9CikKCnNoYXJlZF9nZW5lcyA8LSBwdXJycjo6bWFwKAogIHByb2plY3RfZGZfbGlzdCwgCiAgXChkZikgZGYkZW5zZW1ibF9pZAopCgp0cG1fZGZfaW5kaWNhdG9yX2xpc3QgPC0gcHVycnI6Om1hcDIoCiAgdHBtX2RmX2xpc3QsCiAgc2hhcmVkX2dlbmVzLAogIFwoZGYsIGlkcykgZHBseXI6Om11dGF0ZShkZiwgaW5fcHNldWRvID0gZW5zZW1ibF9pZCAlaW4lIGlkcykKICApCgojIGNsZWFuIHVwIQpybShzaGFyZWRfZ2VuZXMpCnJtKHRwbV9kZl9saXN0KQpgYGAKCgoKIyMgQnVsayBUUE0gZGlzdHJpYnV0aW9ucwoKV2hlbiBlc3RhYmxpc2hpbmcgcHNldWRvYnVsayBkYXRhc2V0cywgZ2VuZXMgd2l0aCBsb3cgZXhwcmVzc2lvbiBsZXZlbHMgd2VyZSByZW1vdmVkLiAKCkJlZm9yZSBsb29raW5nIGF0IHBzZXVkb2J1bGsgbWVhc3VyZXMgaGVyZSwgd2UnbGwgbG9vayBhdCBob3cgdGhlIFRQTSBkaXN0cmlidXRpb25zIChsb2cyKSBjb21wYXJlZCBiZXR3ZWVuIGZvciBnZW5lcyB0aGF0IGFyZSBhbmQgYXJlIG5vdCBpbmNsdWRlZCBpbiB0aGUgcHNldWRvYnVsayBkYXRhc2V0cy4gdGhlIGNvbXBhcmlzb25zLgpXZSBleHBlY3QgdG8gc2VlIHRoYXQgZ2VuZXMgbm90IGluY2x1ZGVkIGluIHRoZSBwc2V1ZG9idWxrIGhhdmUgbG93ZXIgVFBNLgoKCmBgYHtyLCBmaWcud2lkdGggPSAxMiwgIGZpZy5oZWlnaHQgPSA0LCB3YXJuaW5nPUZBTFNFfQp0cG1fZGZfaW5kaWNhdG9yX2xpc3QgfD4KICBwdXJycjo6aW1hcCgKICAgIFwoZGYsIHByb2plY3RfaWQpIHsKICAgICAgCiAgICAgIGdncGxvdChkZikgKyAKICAgICAgICBhZXMoeCA9IGxvZzIoZXhwcmVzc2lvbiksIGZpbGwgPSBpbl9wc2V1ZG8pICsgCiAgICAgICAgZ2VvbV9kZW5zaXR5KGFscGhhID0gMC41KSArIAogICAgICAgIGxhYnMoCiAgICAgICAgICB0aXRsZSA9IHByb2plY3RfaWQsIAogICAgICAgICAgeCA9ICJsb2cyIGJ1bGsgVFBNIgogICAgICAgICkgKwogICAgICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJib3R0b20iKQogICAgfQogICkgfD4KICBwYXRjaHdvcms6OndyYXBfcGxvdHMoZ3VpZGVzID0gImNvbGxlY3QiLCBucm93ID0gMSkgJiB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikKCgpgYGAKCldlIG1vc3RseSBzZWUgdGhlIGV4cGVjdGVkIHRyZW5kOiBHZW5lcyBpbmNsdWRlZCBpbiB0aGUgcHNldWRvYnVsayBjb3VudHMgaGF2ZSBoaWdoZXIgZXhwcmVzc2lvbiBpbiBidWxrIGNvbXBhcmVkIHRvIGdlbmVzIHdoaWNoIHdlcmUgcmVtb3ZlZC4KYFNDUENQMDAwMDA5YCBpcyBhbiBleGNlcHRpb24gaGVyZSwgYnV0IHRoaXMgcHJvamVjdCBoYXMgZmV3ZXIgc2FtcGxlcyBjb21wYXJlZCB0byB0aGUgcmVzdCBzbyB0aGlzIGNvdWxkIGJlIGEgcG93ZXIgYXJ0aWZhY3Q6CgpgYGB7cn0KdHBtX2RmX2luZGljYXRvcl9saXN0IHw+CiAgcHVycnI6Om1hcCgKICAgIFwoZGYpIGxlbmd0aCh1bmlxdWUoZGYkc2FtcGxlX2lkKSkKICApCmBgYAoKUGF1c2UgdG8gY2xlYW4gdXAgYSBsaXR0bGUhCgpgYGB7cn0Kcm0odHBtX2RmX2luZGljYXRvcl9saXN0KQpgYGAKCgojIyBEaXN0cmlidXRpb25zIG9mIHBzZXVkb2J1bGsKCgpGaXJzdCwgbGV0J3MgZ2V0IGEgZ2VuZXJhbCBzZW5zZSBvZiB0aGUgc2NhbGUgb2YgdmFsdWVzIGluIHRoZSBwc2V1ZG9idWxrIHF1YW50aXRpZXM6CgoKYGBge3J9CiMjIGxvZ2NvdW50cwpwcm9qZWN0X2RmX2xpc3QgfD4KICBwdXJycjo6bWFwKAogICAgXChkZikgewogICAgICBkZiB8PiAKICAgICAgICBkcGx5cjo6ZmlsdGVyKGV4cHJlc3Npb25fdHlwZSA9PSAicHNldWRvYnVsa19sb2djb3VudHMiKSB8PgogICAgICAgIGRwbHlyOjpwdWxsKGV4cHJlc3Npb24pIHw+IAogICAgICAgIHN1bW1hcnkoKQogIH0pCmBgYAoKCmBgYHtyfQojIyBkZXNlcQpwcm9qZWN0X2RmX2xpc3QgfD4KICBwdXJycjo6bWFwKAogICAgXChkZikgewogICAgICBkZiB8PiAKICAgICAgICBkcGx5cjo6ZmlsdGVyKGV4cHJlc3Npb25fdHlwZSA9PSAicHNldWRvYnVsa19kZXNlcSIpIHw+CiAgICAgICAgZHBseXI6OnB1bGwoZXhwcmVzc2lvbikgfD4gCiAgICAgICAgc3VtbWFyeSgpCiAgfSkKYGBgCgpUaGUgbG9nY291bnRzIHZlcnNpb24gaGFzIHNvbWUgc3Ryb25nIHJpZ2h0LXNrZXcsIHNvIHdlIHNob3VsZCBsb2cgdGhvc2UgdmFsdWVzIHRvIHN1cHBvcnQgbW9kZWxpbmcuIApXZSBkb24ndCBzZWUgdGhlIHNhbWUgbGV2ZWwgb2Ygc2tldyBmcm9tIERFU2VxLCB3aGljaCBpcyBnb29kIHNpbmNlIHdlIGRvbid0IGV4cGVjdCB0byBzZWUgaXQgb3V0IG9mIHRoYXQgbm9ybWFsaXphdGlvbiBwcm9jZWR1cmU7IHdlIGRvbid0IG5lZWQgdG8gdHJhbnNmb3JtIHRob3NlLgoKTGV0J3MgdGhlcmVmb3JlIHVwZGF0ZSBleHByZXNzaW9uIHRvIGJlIGxvZzIgZm9yIGJvdGggVFBNIGFuZCBwc2V1ZG9idWxrIGxvZ2NvdW50cyAoaS5lLiwgZm9yIGFsbCBidXQgcHNldWRvYnVsayBkZXNlcSkgc2luY2Ugd2UnbGwgdXNlIHRob3NlIHNjYWxlcyBtb3ZpbmcgZm9yd2FyZDoKCmBgYHtyIHdhcm5pbmcgPSBGQUxTRX0KcHJvamVjdF9kZl9saXN0IDwtIHByb2plY3RfZGZfbGlzdCB8PgogIHB1cnJyOjptYXAoCiAgICBcKGRmKSB7CiAgICAgICAgZGYgfD4KICAgICAgICAgIGRwbHlyOjptdXRhdGUoZXhwcmVzc2lvbiA9IGlmZWxzZSgKICAgICAgICAgICAgc3RyaW5ncjo6c3RyX2RldGVjdChleHByZXNzaW9uX3R5cGUsICJkZXNlcSIpLAogICAgICAgICAgICBleHByZXNzaW9uLAogICAgICAgICAgICBsb2cyKGV4cHJlc3Npb24pICMgVE9ETzogZG8gd2Ugd2FudCBhIGZ1ZGdlIGhlcmU/CiAgICAgICAgICApKQogICAgfQogICkKYGBgCgpOb3csIHdlIGNhbiB2aXN1YWxpemUgZGlzdHJpYnV0aW9ucyBvZiBhbGwgcXVhbnRpdGllczoKCmBgYHtyLCBmaWcud2lkdGggPSAxMiwgZmlnLmhlaWdodCA9IDh9CnByb2plY3RfZGZfbGlzdCB8PgogIHB1cnJyOjppbWFwKAogICAgXChkZiwgcHJvamVjdF9pZCkgewogICAgICAKICAgICBnZ3Bsb3QoZGYpICsgCiAgICAgICAgYWVzKHggPSBleHByZXNzaW9uLCBmaWxsID0gZXhwcmVzc2lvbl90eXBlKSArIAogICAgICAgIGdlb21fZGVuc2l0eShhbHBoYSA9IDAuNSkgKyAKICAgICAgICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlID0gIkRhcmsyIikgKyAKICAgICAgICBmYWNldF93cmFwKHZhcnMoZXhwcmVzc2lvbl90eXBlKSwgc2NhbGVzID0gImZyZWUiLCBucm93ID0gMykgKwogICAgICAgIGdndGl0bGUocHJvamVjdF9pZCkgKwogICAgICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikKICAgIH0KICApIHw+CiAgcGF0Y2h3b3JrOjp3cmFwX3Bsb3RzKGd1aWRlcyA9ICJjb2xsZWN0IiwgbnJvdyA9IDEpCmBgYAoKQm90aCB0aGUgcHNldWRvYnVsayBkaXN0cmlidXRpb25zIGxvb2sgcmVhc29uYWJsZSBlbm91Z2ghCgoKIyMgUmVsYXRpb25zaGlwIGJldHdlZW4gcXVhbnRpdGllcwoKTm93IGNvbWUgdGhlIG1hbnkgcGxvdHM6IFdoYXQgZG9lcyB0aGUgcmVsYXRpb25zaGlwIGxvb2sgbGlrZSBiZXR3ZWVuIGJ1bGsgYW5kIGVhY2ggZmxhdm9yIG9mIHBzZXVkb2J1bGs/CldlJ2xsIHZpc3VhbGl6ZSB0aGlzIHBlciBzYW1wbGUuCgpXZSdsbCBmaXJzdCBtYWtlIGEgcGl2b3RlZCB2ZXJzaW9uIG9mIHRoaXMgZGF0YSB0byBlbmFibGUgcGxvdHRpbmcgYW5kIG1vZGVsaW5nLgoKCmBgYHtyfQpwcm9qZWN0X2RmX3dpZGVfbGlzdCA8LSBwcm9qZWN0X2RmX2xpc3QgfD4KICBwdXJycjo6aW1hcCgKICAgIFwoZGYsIHByb2plY3RfaWQpIHsKICAgICAgIGRmIHw+IAogICAgICAgIHRpZHlyOjpwaXZvdF93aWRlcigKICAgICAgICAgIG5hbWVzX2Zyb20gPSBleHByZXNzaW9uX3R5cGUsIAogICAgICAgICAgdmFsdWVzX2Zyb20gPSBleHByZXNzaW9uCiAgICAgICAgKQogICAgfSkKYGBgCgoKTmV4dCwgd2UgaGF2ZSBtYW55IHBsb3RzLCBvcmdhbml6ZWQgYnkgcHJvamVjdDoKCiogTGVmdC1zaWRlIHBhbmVscyBhcmUgYGJ1bGsgdHBtIH4gZGVzZXEgcHNldWRvY291bnRzYAoqIENlbnRlciBwYW5lbHMgYXJlIGBidWxrIHRwbSB+IGxvZ2NvdW50cyBwc2V1ZG9jb3VudHNgCiogUmlnaHQtc2lkZSBwYW5lbHMgYXJlIGBkZXNlcSBwc2V1ZG9jb3VudHMgfiBsb2djb3VudHMgcHNldWRvY291bnRzYCwgdG8gYXNzZXNzIHRoZWlyIHNpbWlsYXJpdHkgZGlyZWN0bHkKCmBgYHtyIGZpZy5oZWlnaHQ9MzAsIGZpZy53aWR0aD0xOCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KcHJvamVjdF9kZl93aWRlX2xpc3QgfD4KICBwdXJycjo6aW1hcCgKICAgIFwoZGYsIHByb2plY3RfaWQpIHsKICAgICAgCiAgICAgIHAxIDwtIGdncGxvdChkZikgKyAKICAgICAgICBhZXMoeCA9IHBzZXVkb2J1bGtfZGVzZXEsIHkgPSBidWxrX3RwbSkgKyAKICAgICAgICBnZW9tX3BvaW50KGFscGhhID0gMC4yLCBzaXplID0gMC41KSArIAogICAgICAgIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIpICsgCiAgICAgICAgZmFjZXRfd3JhcCh2YXJzKHNhbXBsZV9pZCksIG5yb3cgPSA1KSArCiAgICAgICAgZ2d0aXRsZSgiYnVsayB0cG0gfiBkZXNlcSIpCiAgICAgIAogICAgICBwMiA8LSBnZ3Bsb3QoZGYpICsgCiAgICAgICAgYWVzKHggPSBwc2V1ZG9idWxrX2xvZ2NvdW50cywgeSA9IGJ1bGtfdHBtKSArIAogICAgICAgIGdlb21fcG9pbnQoYWxwaGEgPSAwLjIsIHNpemUgPSAwLjUpICsgCiAgICAgICAgZ2VvbV9zbW9vdGgobWV0aG9kID0gImxtIikgKwogICAgICAgIGZhY2V0X3dyYXAodmFycyhzYW1wbGVfaWQpLCBucm93ID0gNSkgKwogICAgICAgIGdndGl0bGUoImJ1bGsgdHBtIH4gbG9nY291bnRzIikgCiAgICAgIAogICAgICAgcDMgPC0gZ2dwbG90KGRmKSArIAogICAgICAgIGFlcyh4ID0gcHNldWRvYnVsa19sb2djb3VudHMsIHkgPSBwc2V1ZG9idWxrX2Rlc2VxKSArIAogICAgICAgIGdlb21fcG9pbnQoYWxwaGEgPSAwLjIsIHNpemUgPSAwLjUpICsgCiAgICAgICAgZ2VvbV9zbW9vdGgobWV0aG9kID0gImxtIikgKwogICAgICAgIGZhY2V0X3dyYXAodmFycyhzYW1wbGVfaWQpLCBucm93ID0gNSkgKwogICAgICAgIGdndGl0bGUoImRlc2VxIH4gbG9nY291bnRzIikgCiAgICAgICAgICAgCiAgICAgIAogICAgICBwYXRjaHdvcms6OndyYXBfcGxvdHMocDEsIHAyLCBwMykgCgogICAgfQogICkgfD4KICBwYXRjaHdvcms6OndyYXBfcGxvdHMobnJvdyA9IDUpCmBgYAoKT24gdGhlIHdob2xlLCB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gYnVsayBhbmQgcHNldWRvYnVsayBsb29rIGV4Y2VwdGlvbmFsbHkgc2ltaWxhciByZWdhcmRsZXNzIG9mIHdoaWNoIHBzZXVkb2J1bGsgcXVhbnRpdHkgaXMgdXNlZCEKT24gdGhlIHJpZ2h0LXNpZGUsIHdlIHNlZSB0aGF0IGluZGVlZCB0aGUgcHNldWRvYnVsayB2ZXJzaW9ucyBhcmUgaGlnaGx5IHNpbWlsYXIgdG8gb25lIGFub3RoZXIgKHJpZ2h0LXNpZGUgcGFuZWxzKS4KVGhlIG1haW4gZGlmZmVyZW5jZSB3ZSBjYW4gcGljayBvdXQgZnJvbSB0aGVzZSBwbG90cyBpcyB0aGF0IHRoZSBgbG9nY291bnRzYCB2ZXJzaW9uIGhhcyBhIGJhbmQgb2YgbG93bHktZXhwcmVzc2VkIGdlbmVzIHdoaWNoIHRoZSBgZGVzZXFgIHZlcnNpb24gZG9lc24ndCBhcHBlYXIgdG8gc2hvdywgd2hpY2ggYWdhaW4gaXMgY29uc2lzdGVudCB3aXRoIHRoZWlyIGRpZmZlcmVudCBvcmlnaW5zLgoKClRvIGlkZW50aWZ5IGlmIHRoZXJlJ3MgYSBtZWFuaW5nZnVsIGJlbmVmaXQgdG8gdXNpbmcgb25lIHBzZXVkb2J1bGsgdmVyc2lvbiBvdmVyIGFub3RoZXIsIHdlJ2xsIGJ1aWxkIGxpbmVhciBtb2RlbHMgb2YgYHRwbSB+IHBzZXVkb2J1bGsgKiBzYW1wbGVgOyB3ZSBjYW4gYXNzdW1lIGFuIGludGVyYWN0aW9uIGJhc2VkIG9uIHRoZSBzbG9wZXMgaW4gdGhlIGFib3ZlIHNjYXR0ZXJwbG90cy4KTm90ZSB0aGF0IHRoZXNlIGFyZSBub3QgZmluYWwgbW9kZWxzIGZvciBhbmFseXNpcywgYnV0IG9ubHkgdXNlZCB0byBndWlkZSBvdXIgbmV4dCBzdGVwcyBvZiBhY3R1YWxseSBidWlsZGluZy9maW5lLXR1bmluZyB0aGUgbW9kZWxzLgoKV2UnbGwgYWxzbyBoYXZlIGEgbG9vayBhdCB0aGUgZGlzdHJpYnV0aW9ucyBvZiBtb2RlbCByZXNpZHVhbHMgYWxvbmcgdGhlIHdheS4KCmBgYHtyIGZpZy5oZWlnaHQgPSAyLCBmaWcud2lkdGggPSA1fQpwcm9qZWN0X2RmX3dpZGVfbGlzdCB8PgogIHB1cnJyOjptYXAoCiAgICBcKGRmKSB7CiAgICAgIAogICAgICAjIEJ1aWxkIG1vZGVscwogICAgICAjIFdlIGZpcnN0IG5lZWQgdG8gcmVtb3ZlIGFueSB1bmRlZmluZWQgdmFsdWVzIGZvciBlYWNoIG1vZGVsCiAgICAgIGRmX2Rlc2VxIDwtIGRmIHw+CiAgICAgICAgZHBseXI6OmZpbHRlcihpcy5maW5pdGUocHNldWRvYnVsa19kZXNlcSksIGlzLmZpbml0ZShidWxrX3RwbSkpCiAgICAgIGZpdF9kZXNlcSA8LSBsbShidWxrX3RwbSB+IHBzZXVkb2J1bGtfZGVzZXEgKiBzYW1wbGVfaWQsIGRhdGEgPSBkZl9kZXNlcSkKICAgIAogICAgICBkZl9sb2djb3VudHMgPC0gZGYgfD4KICAgICAgICBkcGx5cjo6ZmlsdGVyKGlzLmZpbml0ZShwc2V1ZG9idWxrX2xvZ2NvdW50cyksIGlzLmZpbml0ZShidWxrX3RwbSkpCiAgICAgIGZpdF9sb2djb3VudHMgPC0gbG0oYnVsa190cG0gfiBwc2V1ZG9idWxrX2xvZ2NvdW50cyAqIHNhbXBsZV9pZCwgZGF0YSA9IGRmX2xvZ2NvdW50cykKICAgICAgCiAgICAgICMgVGFidWxhdGUgc29tZSBmaXQgc3RhdHMKICAgICAgZml0X3RhYmxlIDwtIHRpYmJsZTo6dHJpYmJsZSgKICAgICAgICB+ZXhwcmVzc2lvbl90eXBlLCB+cnNxdWFyZWQsIH5yZXNpZHVhbF9zZCwgCiAgICAgICAgImRlc2VxIiwgYnJvb206OmdsYW5jZShmaXRfZGVzZXEpJHIuc3F1YXJlZCwgYnJvb206OmdsYW5jZShmaXRfZGVzZXEpJHNpZ21hLCAKICAgICAgICAibG9nY291bnRzIiwgYnJvb206OmdsYW5jZShmaXRfbG9nY291bnRzKSRyLnNxdWFyZWQsIGJyb29tOjpnbGFuY2UoZml0X2xvZ2NvdW50cykkc2lnbWEKICAgICAgKQogICAgICAKICAgICAgCiAgICAgICMgVGFidWxhdGUgc29tZSBmaXQgc3RhdHMsIHdpZGUgZm9yIGVhc2llciB2aWV3aW5nCiAgICAgIGJyb29tX2Rlc2VxIDwtIGJyb29tOjpnbGFuY2UoZml0X2Rlc2VxKQogICAgICBicm9vbV9sb2djb3VudHMgPC0gYnJvb206OmdsYW5jZShmaXRfbG9nY291bnRzKQogICAgICBmaXRfdGFibGUgPC0gZGF0YS5mcmFtZSgKICAgICAgICBkZXNlcV9yc3F1YXJlZCAgICAgICAgPSBicm9vbV9kZXNlcSRyLnNxdWFyZWQsCiAgICAgICAgbG9nY291bnRzX3JzcXVhcmVkICAgID0gYnJvb21fbG9nY291bnRzJHIuc3F1YXJlZCwKICAgICAgICBkZXNlcV9yZXNpZHVhbF9zZCAgICAgPSBicm9vbV9kZXNlcSRzaWdtYSwKICAgICAgICBsb2djb3VudHNfcmVzaWR1YWxfc2QgPSBicm9vbV9sb2djb3VudHMkc2lnbWEgICAgCiAgICAgICkKICAgICAgCiAgICAgICMgUGxvdCB0aGUgcmVzaWR1YWxzIGFzIHdlbGwgb24gdGhlIHdheSBkb3duCiAgICAgIGRlc2VxX2F1Z21lbnQgPC0gYnJvb206OmF1Z21lbnQoZml0X2Rlc2VxKQogICAgICBsb2djb3VudHNfYXVnbWVudCA8LSBicm9vbTo6YXVnbWVudChmaXRfbG9nY291bnRzKQogICAgICAKICAgICAgcDEgPC0gZ2dwbG90KGRlc2VxX2F1Z21lbnQpICsgCiAgICAgICAgYWVzKHggPSBwc2V1ZG9idWxrX2Rlc2VxLCB5ID0gLnJlc2lkKSArIAogICAgICAgIGdlb21fcG9pbnQoc2l6ZSA9IDAuNSwgYWxwaGEgPSAwLjUpICsgCiAgICAgICAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gMCwgY29sb3IgPSAicmVkIikgKwogICAgICAgIGdndGl0bGUoImRlc2VxIikKICAgICAgcDIgPC0gZ2dwbG90KGxvZ2NvdW50c19hdWdtZW50KSArIAogICAgICAgIGFlcyh4ID0gcHNldWRvYnVsa19sb2djb3VudHMsIHkgPSAucmVzaWQpICsgCiAgICAgICAgZ2VvbV9wb2ludChzaXplID0gMC41LCBhbHBoYSA9IDAuNSkgKyAKICAgICAgICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAwLCBjb2xvciA9ICJyZWQiKSArCiAgICAgICAgZ2d0aXRsZSgibG9nY291bnRzIikKICAgICAgCiAgICAgIHByaW50KHBhdGNod29yazo6d3JhcF9wbG90cyhwMSwgcDIsIG5yb3cgPSAxKSkKICAgICAgCiAgICAgICMgYWN0dWFsbHkgcmV0dXJuIHRoZSBmaXQgaW5mbwogICAgICBmaXRfdGFibGUKICAgICAgCiAgICB9CikgfD4KICBwdXJycjo6bGlzdF9yYmluZChuYW1lc190byA9ICJwcm9qZWN0X2lkIikKYGBgCgpNb2RlbHMgZnJvbSBkaWZmZXJlbnQgcHNldWRvYnVsayBxdWFudGl0aWVzIGFyZSBleHBlY3RlZGx5LCBiYXNlZCBvbiBzY2F0dGVycGxvdHMgYWJvdmUsICBtaW5pbWFsLCBhbmQgYmFzZWQgb24gdGhlIHJlc2lkdWFsIHBsb3RzIGFyb3VuZCBsaW5lYXIgbW9kZWwgYXNzdW1wdGlvbnMgc2VlbSByZWFzb25hYmx5IG1ldC4KVGhlIGBERVNlcTJgIGZsYXZvciBhcHBlYXJzIHRvIG1hcmdpbmFsbHkgb3V0cGVyZm9ybSAoYmFzZWQgb24gdGhlc2UgbGltaXRlZCBzdGF0cykgaW4gMy81IHByb2plY3RzLiAKCiMjIENvbmNsdXNpb25zCgpFaXRoZXIgY2FsY3VsYXRpb24gZm9yIHBzZXVkb2J1bGsgYXBwZWFycyB0byBiZSBmaW5lLCBhbmQgaXQncyBsaWtlbHkgdGhhdCB3ZSdkIGdldCByb3VnaGx5IHRoZSBzYW1lIHJlc3VsdHMgZWl0aGVyIHdheS4KSSB3b3VsZCBzdWdnZXN0IHRvIHByb2NlZWQgd2l0aCB0aGUgYERFU2VxMmAgbm9ybWFsaXplZCB2ZXJzaW9uIHNpbmNlIHdlIGRvIG5vdCBuZWVkIGFkZGl0aW9uYWwgYGxvZzJgIHRyYW5zZm9ybWF0aW9ucywgd2hpY2ggbGVhZHMgdG8gbG9zcyBvZiAwLWV4cHJlc3Npb24gZ2VuZXMgaW4gdGhlIG1vZGVsIHNpbmNlIHRoZXkgYXJlIHVuZGVmaW5lZCAodW5sZXNzIHdlIHdhbnQgdG8gZnVkZ2UgZmFjdG9yIHRoZW0sIGJ1dCBhdm9pZGluZyB0aGlzIGlmIHdlIGNhbiBzZWVtcyBiZXN0KS4KCiMjIFNlc3Npb24gaW5mbwoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBg